#include "Avalon/UidService/UidClient.hpp"
#include "Avalon/IO/Channel.hpp"
#include "Avalon/ServiceLocator/AuthenticationException.hpp"
#include "Avalon/ServiceLocator/NotLoggedInException.hpp"
#include "Avalon/ServiceLocator/ServiceLocatorClient.hpp"
#include "Avalon/UidService/UidServiceException.hpp"
#include "Avalon/UidService/UidServices.hpp"

using namespace Avalon;
using namespace Avalon::IO;
using namespace Avalon::Serialization;
using namespace Avalon::ServiceLocator;
using namespace Avalon::Services;
using namespace Avalon::Threading;
using namespace Avalon::UidService;
using namespace boost;
using namespace boost::posix_time;
using namespace boost::signals2;
using namespace std;

UidClient::UidClient(ServiceLocatorClient* serviceClient, Channel* channel,
    DataShuttleFactory* dataShuttleFactory, Timer* heartbeatTimer,
    time_duration timeout, ThreadPool& threadPool)
    : m_serviceClient(serviceClient),
      m_channel(channel),
      m_isConnected(false),
      m_isAuthenticating(false),
      m_isAuthenticated(false),
      m_service(dataShuttleFactory, heartbeatTimer, timeout, threadPool),
      m_nextUid(0),
      m_lastUid(-1),
      m_blockSize(10),
      m_signalHandler(threadPool) {
  SetRequestHandler<GetSessionIdService>(m_service, m_signalHandler,
    bind(&UidClient::OnGetSessionIdRequest, this, _1, _2));
  SetMessageHandler<SessionAuthenticationMessage>(m_service, m_signalHandler,
    bind(&UidClient::OnSessionAuthentication, this, _1, _2));
}

UidClient::~UidClient() {
  Close();
}

long long UidClient::GetNextUid() {
  boost::unique_lock<boost::mutex> lock(m_mutex);
  if(m_nextUid <= m_lastUid) {
    long long uid = m_nextUid;
    ++m_nextUid;
    return uid;
  }
  if(!m_isAuthenticated) {
    BOOST_THROW_EXCEPTION(NotLoggedInException());
  }
  Async<ReserveUidsService::ReturnType> reserveResponse;
  m_service.SendRequest<ReserveUidsService>(m_channel.get(), m_blockSize,
    Store(reserveResponse));
  reserveResponse.Wait(lock);
  const ReserveUidsService::ReturnType& result = reserveResponse.Get();
  if(!result.get<0>()) {
    BOOST_THROW_EXCEPTION(UidServiceException(result.get<1>()));
  }
  m_nextUid = result.get<2>();
  m_lastUid = m_nextUid + m_blockSize - 1;
  m_blockSize = 2 * m_blockSize;
  long long uid = m_nextUid;
  ++m_nextUid;
  return uid;
}

bool UidClient::IsConnected() const {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  return m_isConnected;
}

void UidClient::Connect() {
  boost::unique_lock<boost::mutex> lock(m_mutex);
  if(m_isConnected) {
    return;
  }
  bool isOpen;
  connection closedConnection = m_serviceClient->ConnectClosedSignal(
    m_signalHandler.GetSlot<Connection::ClosedSignal>(
    bind(&UidClient::OnServiceClientClosed, this)), Store(isOpen));
  m_serviceClientState.Initialize(isOpen, closedConnection);
  if(!m_serviceClientState.IsOpen()) {
    BOOST_THROW_EXCEPTION(ConnectException("Service locator not connected."));
  }
  try {
    static_cast<ClientConnection&>(m_channel->GetConnection()).Connect();
  } catch(std::exception&) {
    Shutdown();
    if(m_isConnected) {
      m_closedCondition.wait(lock);
    }
    BOOST_RETHROW;
  }
  closedConnection = m_service.AddChannel(m_channel.get(),
    m_signalHandler.GetSlot<Connection::ClosedSignal>(bind(
    &UidClient::OnConnectionClosed, this)), Store(isOpen));
  m_channelState.Initialize(isOpen, closedConnection);
  if(!m_channelState.IsOpen()) {
    Shutdown();
    if(m_isConnected) {
      m_closedCondition.wait(lock);
    }
    BOOST_THROW_EXCEPTION(ConnectException());
  }
  m_isConnected = true;
  m_isAuthenticating = true;
  m_authenticationCondition.wait(lock);
  assert(!m_isAuthenticating);
  if(!m_isAuthenticated) {
    Shutdown();
    if(m_isConnected) {
      m_closedCondition.wait(lock);
    }
    BOOST_THROW_EXCEPTION(AuthenticationException());
  }
}

void UidClient::Close() {
  boost::unique_lock<boost::mutex> lock(m_mutex);
  if(!m_isConnected) {
    return;
  }
  Shutdown();
  if(m_isConnected) {
    m_closedCondition.wait(lock);
  }
}

connection UidClient::ConnectClosedSignal(
    const ClosedSignal::slot_type& slot) const {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  return m_closedSignal.connect(slot);
}

connection UidClient::ConnectClosedSignal(const ClosedSignal::slot_type& slot,
    Out<bool> isConnected) const {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  *isConnected = m_isConnected;
  return m_closedSignal.connect(slot);
}

void UidClient::Shutdown() {
  m_serviceClientState.SetClosed();
  if(m_channelState.IsOpen()) {
    m_channel->GetConnection().Close();
    return;
  }
  bool wasConnected = m_isConnected;
  m_isAuthenticating = false;
  m_isAuthenticated = false;
  m_isConnected = false;
  m_authenticationCondition.notify_one();
  m_closedCondition.notify_one();
  if(wasConnected) {
    m_closedSignal();
  }
}

void UidClient::OnServiceClientClosed() {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  m_serviceClientState.SetClosed();
  Shutdown();
}

void UidClient::OnConnectionClosed() {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  m_service.RemoveChannel(m_channel.get());
  m_channelState.SetClosed();
  Shutdown();
}

void UidClient::OnGetSessionIdRequest(
    const RequestToken<GetSessionIdService>& request, unsigned int key) {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  request.SendResponse(make_tuple(true, "",
    m_serviceClient->GetEncryptedSessionId(key)));
}

void UidClient::OnSessionAuthentication(Channel* channel, bool passed) {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  m_isAuthenticating = false;
  m_isAuthenticated = passed;
  m_authenticationCondition.notify_one();
}
